import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.*;

@Intercepts({

        @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
        @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class })
})
@Component
public class MybatisExecutorPluginAudit implements Interceptor {
    Logger logger = LoggerFactory.getLogger(MybatisExecutorPluginAudit.class);
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 只调试那些包 针对调试
        String[] filter_package = {
                ""
        };
        // 返回list 限制最大打印20行
        int max_result_row = 20;
        String method = ((MappedStatement) invocation.getArgs()[0]).getId();
        BoundSql boundSql = ((MappedStatement) invocation.getArgs()[0]).getBoundSql(invocation.getArgs()[1]);
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        StringBuilder builder = new StringBuilder();
        ObjectMapper mapper = new ObjectMapper();
        if(result instanceof List){
            builder.append("返回条数: ").append(((List)result).size()) .append("\n");
            int i = 0;
            for(Object f: (List)result){
                i++;
                if(i>max_result_row){
                    break;
                }
                builder.append(mapper.writeValueAsString(f)).append("\n");
            }
        }else{
            builder.append("返回条数：");
            builder.append(result==null?"0":"1");
            builder.append("\n");
            builder.append(mapper.writeValueAsString(result));
            builder.append("\n");
        }
        // 如果 filter_package 指定了包则只打印对应的mybatis日志
        boolean filter = false;
        for(String pkg : filter_package){
            if(method.startsWith(pkg)){
                filter = true;
            }
        }
        if( filter_package.length < 1 || filter) {
            logger.info("耗时：{} \n\tmethod: {}\n\t{}\n{}", System.currentTimeMillis() - start, method, getSql(boundSql), builder);
        }
        return result;
    }

    @Override
    public Object plugin(Object target) {
        // 如果是执行的方法则调用当前对象进行解析
        if (target instanceof Executor)
            return Plugin.wrap(target, this);
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
    }

    /**
     * @author gtinlian
     * @param boundSql
     * @return
     * @Description 获取sql，如果sql "参数"中携带 <?> 则将参数个sql 拼接，如果sql"参数"中不携带 <?> 则完整sql拼接
     *
     */
    String getSql(BoundSql boundSql) {
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        // 获取参数
        List<Object> param = getParameter(boundSql);
        // 渲染参数
        return preparedStatementImpl(sql,param);
    }

    /**
     * @author gtinlian
     * @param boundSql
     * @return
     * @Description sql获取参数列表
     * @author gtinlian
     */
    private List<Object> getParameter(BoundSql boundSql) {
        List<Object> result = new ArrayList<>();
        List<ParameterMapping> mappings = boundSql.getParameterMappings();
        for (ParameterMapping mapping : mappings) {
            // 字段映射实体名
            String propertyName = mapping.getProperty();

            Object value = boundSql.getAdditionalParameter(propertyName);
            Object paramObject = boundSql.getParameterObject();
            try {
                // 如果是单参数没有声明标量时 ParamObject 存储一个值 
                // ParamObject 的数据对象模式   
                // 1： [String] 单个值变量     
                // 2： Map<mapperProperty,value>  储存值或对象   
                // 3: Map<mapperProperty.split("[.]")[0],Entity>
                if (!(paramObject instanceof HashMap)) {
                    if(propertyName != null){
                        try {
                            Method f =paramObject.getClass().getMethod("get"+propertyName.substring(0, 1).toUpperCase()+propertyName.substring(1));
                            f.setAccessible(true);
                            value = f.invoke(paramObject);
                        }catch (Exception e){
                            value = paramObject;
                        }
                    }else {
                        value = paramObject;
                    }
                } else {
                    value = value == null ? ((Map) paramObject).get(propertyName) : value;
                }
            } catch (Exception e) {
                try {
                    String[] param = propertyName.split("[.]");
                    String fileName = null;
                    Object paramObj = paramObject;
                    if (param.length > 1) {
                        for (int i = 0; i < param.length - 1; i++) {
                            try {
                                paramObj = ((Map) paramObj).get(param[i]);
                            } catch (Exception er) {
                                Field field = paramObj.getClass().getDeclaredField(param[i]);
                                field.setAccessible(true);
                                paramObj = field.get(paramObj);
                            }
                        }
                        fileName = param[param.length - 1];
                    }
                    Field field = paramObj.getClass().getDeclaredField(fileName);
                    field.setAccessible(true);
                    value = field.get(paramObj);
                } catch (Exception exception) {
                }
            }
            result.add(value);
        }
        return result;
    }

    /**
     * @author gtinlian
     * 渲染sql 方法
     * @param sql PreparedStatement格式sql
     * @param arr 参数
     * @return
     */
    static String preparedStatementImpl(String sql,List<Object> arr){
        return preparedStatementImpl(sql,arr.toArray());
    }
    static String preparedStatementImpl(String sql,Object ...array) {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        StringBuilder buffer = new StringBuilder();
        // 参数下表
        int i = 0;
        // 是否被标记未字符串
        boolean ingres = false;
        // 字符串结束标识
        char end = '0';
        // 处理转义字符
        boolean convert = false;
        for (char c : sql.toCharArray()) {
            // 处理转义字符
            if (convert) {
                buffer.append(c);
                convert = false;
                continue;
            }
            if (c == '\\') {
                convert = true;
                buffer.append(c);
                continue;
            }
            // 处理屏蔽字符
            if (!ingres && (c == '"' || c == '\'')) {
                end = c;
                ingres = true;
                buffer.append(c);
                continue;
            } else if (ingres) {
                if (c == end) {
                    ingres = false;
                    buffer.append(c);
                    continue;
                }
                buffer.append(c);
                continue;
            }
            // 处理参数
            if (c == '?') {
                if (array[i] instanceof String) {
                    buffer.append("\'");
                    buffer.append(((String) array[i]).replaceAll("\"","\\\"").replaceAll("\'","\\\'"));
                    buffer.append("\'");
                } else if(array[i] instanceof Date){
                    buffer.append("'").append(format.format(array[i])).append("'");
                }else{
                    buffer.append(array[i]);
                }
                i++;
            } else {
                buffer.append(c);
            }
        }
        return buffer.toString();
    }
}